package demo.handler;

import generated.XMLBIBLE;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.jcr.Binary;
import javax.jcr.Node;
import javax.jcr.Value;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.event.EventUtil;
import org.apache.sling.event.jobs.JobProcessor;
import org.apache.sling.event.jobs.JobUtil;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import demo.model.Bible;
import demo.model.Book;
import demo.model.Chapter;
import demo.model.Verse;
import demo.service.BibleUploadServiceImpl;

@SuppressWarnings("restriction")
@Service
@Component(immediate = true)
@Property(name = "event.topics", value = BibleUploadServiceImpl.JOB_TOPIC)
public class BibleUploadEventHandler implements JobProcessor, EventHandler {

	private static final String BAM_XREFS = "/bam/xrefs";

	private static final String BAM_BIBLES = "/bam/bibles";

	private static final Logger log = LoggerFactory
			.getLogger(BibleUploadEventHandler.class);

	@Reference
	private ResourceResolverFactory resolverFactory;

	public void handleEvent(Event event) {
		if (EventUtil.isLocal(event)) {
			JobUtil.processJob(event, this);
		}
	}

	public boolean process(Event event) {
		try {
			String resourcePath = (String) event.getProperty("resourcePath");
			boolean clear = Boolean.parseBoolean(event.getProperty("clear")
					.toString());

			Map<String, Object> props = new HashMap<String, Object>();
			props.put(ResourceResolverFactory.USER, "admin");
			props.put(ResourceResolverFactory.PASSWORD, "admin");
			ResourceResolver resourceResolver = resolverFactory
					.getAdministrativeResourceResolver(props);
			Resource res = resourceResolver.getResource(resourcePath);
			log.info("Processing resource: {}", resourcePath);

			if (ResourceUtil.isA(res, "nt:file")) {
				if (resourcePath.startsWith(BAM_BIBLES)) {
					String mimeType = res.getResourceMetadata()
							.getContentType();
					if ("application/xml".equals(mimeType)) {
						processBibleFile(res, clear);
					}
				} else if (resourcePath.startsWith(BAM_XREFS)) {
					processXrefsFile(res, clear);
				}
			}
			return true;
		} catch (Exception e) {
			log.error("RepositoryException: " + e);
			return false;
		}
	}

	private void processXrefsFile(final Resource res, final boolean clear) {
		InputStream in = null;
		Map<String, List<String>> internalStructure = new HashMap<String, List<String>>();
		int counter = 0;
		BufferedReader br;
		boolean end = false;
		try {
			Node content = res.getChild("jcr:content").adaptTo(Node.class);
			Binary binaryContent = content.getProperty("jcr:data").getBinary();
			in = binaryContent.getStream();
			br = new BufferedReader(new InputStreamReader(in));
			String line = br.readLine();

			Node newXrefNode = null;
			Resource xrefsRootResource = res.getResourceResolver().getResource(
					"/content");
			Node xrefsRootNode = xrefsRootResource.adaptTo(Node.class);

			if (clear) {
				newXrefNode = forceCreateNode(xrefsRootNode, "xrefs");
			} else {
				newXrefNode = getOrCreateNode(xrefsRootNode, "xrefs");
			}

			log.info("Start parsing raw reference in internal structure");

			while (!end) {
				internalStructure.clear();

				while (line != null) {
					String[] rawReference = line.split("\t");
					String verse = rawReference[0];
					String[] reference = rawReference[1].toLowerCase().split(
							"-");

					List<String> references = internalStructure.get(verse);
					if (references == null) {
						references = new ArrayList<String>();
						internalStructure.put(verse, references);
					}
					
					for (String ref : reference) {
						// Build verse path
						ref = ref.replaceAll("\\.", "/");
						counter++;
						references.add(ref);
					}
					line = br.readLine();
					if (counter % 5000 == 0)
						break;
				}

				end = line == null;

				log.info("Parsed {} verses", counter);

				for (String v : internalStructure.keySet()) {
					String[] verseFrom = v.split("\\.");
					Node book = getOrCreateNode(newXrefNode,
							verseFrom[0].toLowerCase());
					Node chapter = getOrCreateNode(book, verseFrom[1]);
					Node verse = getOrCreateNode(chapter, verseFrom[2]);

					List<String> refs = internalStructure.get(v);
					String[] newReferences = new String[refs.size()];
					refs.toArray(newReferences);
					String[] references = mergeReferences(verse, newReferences);

					verse.setProperty("references", references);
					verse.setProperty("sling:resourceType",
							"/apps/godswill/xrefs");

					verse.getSession().save();
				}
				System.gc();
			}
			log.info("End parsing raw reference in internal structure");
		} catch (Exception e) {
			log.error(e.getMessage(), e);
		} finally {
			if (in != null) {
				try {
					in.close();
					in = null;
				} catch (Exception e) {
					log.error(e.getMessage(), e);
				}
			}
			// Run garbage collection
			System.gc();
		}

	}

	private String[] mergeReferences(final Node verse,
			final String[] newReferences) {
		String[] result = newReferences;
		try {
			Value[] values = verse.getProperty("references").getValues();
			if (values != null) {
				result = new String[newReferences.length + values.length];
				System.arraycopy(newReferences, 0, result, 0,
						newReferences.length);
				int start = newReferences.length;
				for (Value ref : values) {
					result[start] = ref.getString();
					start++;
				}
			} else {
				result = newReferences;
			}

		} catch (Exception e) {

		}
		return result;
	}

	private void processBibleFile(final Resource res, final boolean clear) {
		InputStream in = null;
		Node newBibleNode = null;
		XMLBIBLE xmlBible = null;

		try {
			JAXBContext context = JAXBContext.newInstance(XMLBIBLE.class);
			Unmarshaller um = context.createUnmarshaller();
			Node content = res.getChild("jcr:content").adaptTo(Node.class);
			Binary binaryContent = content.getProperty("jcr:data").getBinary();
			in = binaryContent.getStream();

			xmlBible = (XMLBIBLE) um.unmarshal(in);
			log.info("Bible name is: " + xmlBible.getBiblename());

			String filename = res.getName();
			Bible bible = Bible.createFromZefania(filename.split("\\.")[0],
					xmlBible);
			xmlBible = null;
			// Free resources
			System.gc();
			Resource bibleRootResource = res.getResourceResolver().getResource(
					"/content/bibles");
			Node bibleRootNode = bibleRootResource.adaptTo(Node.class);

			// Create new bible node and set properties
			if (clear) {
				newBibleNode = forceCreateNode(bibleRootNode, bible.id);
			} else {
				newBibleNode = getOrCreateNode(bibleRootNode, bible.id);
			}
			newBibleNode.setPrimaryType("sling:Folder");
			newBibleNode.setProperty("name", bible.biblename);
			newBibleNode.setProperty("title", bible.title);
			newBibleNode.setProperty("description", bible.description);
			newBibleNode.setProperty("contentType", "bible");

			String bibleSID = bible.id.toLowerCase();

			newBibleNode.setProperty("sid", bibleSID);
			newBibleNode.setProperty("sling:resourceType",
					"/apps/godswill/bible");

			// Add books
			for (Book book : bible.books) {
				// Create book
				String bookSID = bibleSID + "/" + book.id;
				Node newBookNode = getOrCreateNode(newBibleNode, book.id);
				newBookNode.setPrimaryType("sling:Folder");
				newBookNode.setProperty("name", book.name);
				newBookNode.setProperty("sid", bookSID.toLowerCase());
				newBookNode.setProperty("bookNumber", book.bookNumber);
				newBookNode.setProperty("contentType", "book");
				newBookNode.setProperty("sling:resourceType",
						"/apps/godswill/book");

				log.info("Start adding book {}", book.name);

				// Add chapters
				for (Chapter chapter : book.chapters) {
					String chapterSID = bookSID + "/" + chapter.id;
					Node newChapterNode = getOrCreateNode(newBookNode,
							chapter.id.toString());
					newChapterNode.setPrimaryType("sling:Folder");
					newChapterNode.setProperty("sid", chapterSID.toLowerCase());
					newChapterNode.setProperty("contentType", "chapter");
					newChapterNode.setProperty("sling:resourceType",
							"/apps/godswill/chapter");

					// Add verses
					for (Verse verse : chapter.verses) {
						String verseSID = chapterSID + "/" + verse.id;
						Node newVerseNode = getOrCreateNode(newChapterNode,
								verse.id.toString());
						newVerseNode.setPrimaryType("sling:Folder");
						newVerseNode.setProperty("content", verse.content);
						newVerseNode.setProperty("contentType", "verse");
						newVerseNode.setProperty("sid", verseSID.toLowerCase());
						newVerseNode.setProperty("sling:resourceType",
								"/apps/godswill/verse");
					}

				}

				log.info("End adding book {}", book.name);
				// Save after each book
				newBookNode.getSession().save();
				log.info("book {} saved", book.name);
			}

			newBibleNode.getSession().save();
			log.info("The file {} has been processed. Path {} ", filename,
					newBibleNode.getPath());

		} catch (Exception e) {
			log.error(e.getMessage(), e);
		} finally {
			newBibleNode = null;
			xmlBible = null;
			if (in != null) {
				try {
					in.close();
					in = null;
				} catch (Exception e) {
					log.error(e.getMessage(), e);
				}
			}
			// Run garbage collection
			System.gc();
		}
	}

	private static Node forceCreateNode(final Node parent, final String id)
			throws Exception {
		try {
			log.info("Start deleting node {}", id);
			parent.getNode(id).remove();
			parent.getSession().save();
			log.info("End deleting node {}", id);
		} catch (Exception e) {
			log.info("Could not deleting node {}", id, e);
		}
		return parent.addNode(id);
	}

	private static Node getOrCreateNode(final Node parent, final String id)
			throws Exception {
		Node result = null;
		try {
			result = parent.getNode(id);
		} catch (Exception e) {

		}

		if (result == null) {
			result = parent.addNode(id);
		}

		return result;
	}

}
